﻿using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace ZyGame
{
    public enum UnityEventNames
    {
        OnTriggerExit2D,
        OnTriggerExit,
        OnTriggerEnter2D,
        OnTriggerEnter,
        OnMouseUpAsButton,
        OnMouseUp,
        OnMouseOver,
        OnMouseExit,
        OnMouseEnter,
        OnMouseDrag,
        OnMouseDown,
        OnEnable,
        OnDrawGizmosSelected,
        OnDrawGizmos,
        OnDisable,
        OnControllerColliderHit,
        OnCollisionExit2D,
        OnCollisionExit,
        OnCollisionEnter2D,
        OnCollisionEnter,
        OnBecameVisible,
        OnBecameInvisible,
        OnApplicationQuit,
        OnApplicationFocus,
        OnDestroy,
    }

    class UnityHandler : MonoBehaviour
    {
        private static UnityHandler _instance;


        private event Action late;
        private event Action update;
        private event Action fixedUpdate;
        private Action<Collision2D> Collision2D_stay;
        private Action<Collision> Collision_stay;
        private Action<Collider> Collider_stay;
        private Action<Collider2D> Collider2D_stay;
        private static Action<Coroutine> conoutineStopup;
        private static Action<IEnumerator> conoutineStartup;
        private Queue<Action> inMainThread = new Queue<Action>();
        private List<CountDownSchedule> countdowns = new List<CountDownSchedule>();
        private Dictionary<UnityEventNames, Action<object>> callbackList = new Dictionary<UnityEventNames, Action<object>>();
        private List<ISchedule> schedules = new List<ISchedule>();

        public static UnityHandler instance
        {
            get
            {
                if (_instance == null)
                {
                    _instance = new GameObject("CoroutineRunner").AddComponent<UnityHandler>();
                    AddUnityEventListener(_instance.gameObject, UnityEventNames.OnApplicationQuit, args => Client.Events.Notify("APP_QUIT"));
                    AddUnityEventListener(_instance.gameObject, UnityEventNames.OnApplicationQuit, args => Client.Events.Notify("APP_FOCUS", args));
                    GameObject.DontDestroyOnLoad(_instance.gameObject);
                }

                return _instance;
            }
        }

        private void FixedUpdate()
        {
            try
            {
                fixedUpdate?.Invoke();
                for (int i = countdowns.Count - 1; i >= 0; i--)
                {
                    countdowns[i].Execute();
                    if (countdowns[i].isCompletion || countdowns[i].HasCanceled())
                    {
                        countdowns.Remove(countdowns[i]);
                    }
                }
            }
            catch (Exception e)
            {
                Client.Console.WriteError(e);
            }
        }

        private void Update()
        {
            update?.Invoke();
            if (schedules.Count > 0)
            {
                for (int i = schedules.Count - 1; i >= 0; i--)
                {
                    if (schedules[i].isCompletion)
                    {
                        schedules.Remove(schedules[i]);
                        continue;
                    }

                    try
                    {
                        schedules[i].Execute();
                    }
                    catch (Exception e)
                    {
                        Client.Console.WriteError(e);
                    }
                }
            }

            if (inMainThread.Count > 0)
            {
                while (inMainThread.TryDequeue(out Action action))
                {
                    action?.Invoke();
                }
            }
        }

        private void OnApplicationQuit()
        {
            TriggerUnityEvent(UnityEventNames.OnApplicationQuit);
        }

        private void OnApplicationFocus(bool focus)
        {
            TriggerUnityEvent(UnityEventNames.OnApplicationFocus, focus);
        }

        private void OnDestroy()
        {
            TriggerUnityEvent(UnityEventNames.OnDestroy);
        }

        // 如果启用 Behaviour，则在每一帧都将调用 LateUpdate
        private void LateUpdate()
        {
            late?.Invoke();
        }

        // 当呈现器在任何照相机上都不可见时调用 OnBecameInvisible
        private void OnBecameInvisible()
        {
            TriggerUnityEvent(UnityEventNames.OnBecameInvisible);
        }

        // 当呈现器在任何照相机上可见时调用 OnBecameVisible
        private void OnBecameVisible()
        {
            TriggerUnityEvent(UnityEventNames.OnBecameVisible);
        }

        // 当此碰撞器/刚体开始接触另一个刚体/碰撞器时，调用 OnCollisionEnter
        private void OnCollisionEnter(Collision collision)
        {
            TriggerUnityEvent(UnityEventNames.OnCollisionEnter, collision);
        }

        // 当此碰撞器 2D/刚体 2D 开始接触另一刚体 2D/碰撞器 2D 时调用 OnCollisionEnter2D (仅限 2D 物理)
        private void OnCollisionEnter2D(Collision2D collision)
        {
            TriggerUnityEvent(UnityEventNames.OnCollisionEnter2D, collision);
        }

        // 当此碰撞器/刚体停止接触另一刚体/碰撞器时调用 OnCollisionExit
        private void OnCollisionExit(Collision collision)
        {
            TriggerUnityEvent(UnityEventNames.OnCollisionExit, collision);
        }

        // 当此碰撞器 2D/刚体 2D 停止接触另一刚体 2D/碰撞器 2D 时调用 OnCollisionExit2D (仅限 2D 物理)
        private void OnCollisionExit2D(Collision2D collision)
        {
            TriggerUnityEvent(UnityEventNames.OnCollisionExit2D, collision);
        }

        // 每当此碰撞器/刚体接触到刚体/碰撞器时，OnCollisionStay 将在每一帧被调用一次
        private void OnCollisionStay(Collision collision)
        {
            Collision_stay?.Invoke(collision);
        }

        // 每当碰撞器 2D/刚体 2D 接触到刚体 2D/碰撞器 2D 时，OnCollisionStay2D 将在每一帧被调用一次(仅限 2D 物理)
        private void OnCollisionStay2D(Collision2D collision)
        {
            Collision2D_stay?.Invoke(collision);
        }

        // 如果控制器在执行移动时击中碰撞器，则调用 OnControllerColliderHit
        private void OnControllerColliderHit(ControllerColliderHit hit)
        {
            TriggerUnityEvent(UnityEventNames.OnControllerColliderHit, hit);
        }

        // 当行为被禁用或处于非活动状态时调用此函数
        private void OnDisable()
        {
            TriggerUnityEvent(UnityEventNames.OnDisable);
        }

        // 如果你想要绘制可选取并始终绘制的 gizmos，请实现此 OnDrawGizmos
        private void OnDrawGizmos()
        {
            TriggerUnityEvent(UnityEventNames.OnDrawGizmos);
        }

        // 如果你希望仅在对象被选中时绘制 gizmos，请实现此 OnDrawGizmosSelected
        private void OnDrawGizmosSelected()
        {
            TriggerUnityEvent(UnityEventNames.OnDrawGizmosSelected);
        }

        // 当对象已启用并处于活动状态时调用此函数
        private void OnEnable()
        {
            TriggerUnityEvent(UnityEventNames.OnEnable);
        }

        // 当用户在 GUIElement 或碰撞器上按鼠标按钮时调用 OnMouseDown
        public void OnMouseDown()
        {
            TriggerUnityEvent(UnityEventNames.OnMouseDown);
        }

        // 当用户在 GUIElement 或碰撞器上单击鼠标并保持按住鼠标时调用 OnMouseDrag
        public void OnMouseDrag()
        {
            TriggerUnityEvent(UnityEventNames.OnMouseDrag);
        }

        // 当鼠标进入 GUIElement 或碰撞器时调用 OnMouseEnter
        public void OnMouseEnter()
        {
            TriggerUnityEvent(UnityEventNames.OnMouseEnter);
        }

        // 当鼠标不再停留在 GUIElement 或碰撞器上时调用 OnMouseExit
        public void OnMouseExit()
        {
            TriggerUnityEvent(UnityEventNames.OnMouseExit);
        }

        // 当鼠标停留在 GUIElement 或碰撞器上时每帧都调用 OnMouseOver
        public void OnMouseOver()
        {
            TriggerUnityEvent(UnityEventNames.OnMouseOver);
        }

        // 当用户松开鼠标按钮时调用 OnMouseUp
        public void OnMouseUp()
        {
            TriggerUnityEvent(UnityEventNames.OnMouseUp);
        }

        // 仅当在同一 GUIElement 或碰撞器上按下鼠标，在松开时调用 OnMouseUpAsButton
        public void OnMouseUpAsButton()
        {
            TriggerUnityEvent(UnityEventNames.OnMouseUpAsButton);
        }

        // 如果另一个碰撞器进入了触发器，则调用 OnTriggerEnter
        public void OnTriggerEnter(Collider other)
        {
            TriggerUnityEvent(UnityEventNames.OnTriggerEnter, other);
        }

        // 如果另一个碰撞器 2D 进入了触发器，则调用 OnTriggerEnter2D (仅限 2D 物理)
        public void OnTriggerEnter2D(Collider2D collision)
        {
            TriggerUnityEvent(UnityEventNames.OnTriggerEnter2D, collision);
        }

        // 如果另一个碰撞器停止接触触发器，则调用 OnTriggerExit
        public void OnTriggerExit(Collider other)
        {
            TriggerUnityEvent(UnityEventNames.OnTriggerExit, other);
        }

        // 如果另一个碰撞器 2D 停止接触触发器，则调用 OnTriggerExit2D (仅限 2D 物理)
        public void OnTriggerExit2D(Collider2D collision)
        {
            TriggerUnityEvent(UnityEventNames.OnTriggerExit2D, collision);
        }

        // 对于触动触发器的所有“另一个碰撞器”，OnTriggerStay 将在每一帧被调用一次
        public void OnTriggerStay(Collider other)
        {
            Collider_stay?.Invoke(other);
        }

        // 如果其他每个碰撞器 2D 接触触发器，OnTriggerStay2D 将在每一帧被调用一次(仅限 2D 物理)
        public void OnTriggerStay2D(Collider2D collision)
        {
            Collider2D_stay?.Invoke(collision);
        }

        private void TriggerUnityEvent(UnityEventNames eventNames, object args = default)
        {
            if (callbackList.TryGetValue(eventNames, out Action<object> callback))
            {
                try
                {
                    callback?.Invoke(args);
                }
                catch (Exception e)
                {
                    Client.Console.WriteError(e);
                }
            }
        }


        public static void AddUnityEventListener(GameObject gameObject, UnityEventNames eventNames, Action<object> action)
        {
            if (gameObject is null)
            {
                return;
            }

            UnityHandler handler = gameObject.GetComponent<UnityHandler>();
            if (handler is null)
            {
                handler = gameObject.AddComponent<UnityHandler>();
            }

            if (handler.callbackList.ContainsKey(eventNames) is false)
            {
                handler.callbackList.Add(eventNames, action);
            }
            else
            {
                handler.callbackList[eventNames] += action;
            }
        }

        public static void RunningInMainThread(Action action)
        {
            instance.inMainThread.Enqueue(action);
        }

        public static void AddLate(Action action)
        {
            RemoveCallback(action);
            instance.late += action;
        }

        public static void AddUpdate(Action onUpdateCallback)
        {
            RemoveCallback(onUpdateCallback);
            instance.update += onUpdateCallback;
        }

        public static void RemoveCallback(Action callback)
        {
            instance.update -= callback;
            instance.fixedUpdate -= callback;
            instance.late -= callback;
        }

        public static void AddFixedUpdate(Action callback)
        {
            RemoveCallback(callback);
            instance.fixedUpdate += callback;
        }

        public static void ListenDestory(GameObject gameObject, Action action)
        {
            AddUnityEventListener(gameObject, UnityEventNames.OnDestroy, args => action());
        }

        public static void ListenDestory<T>(GameObject gameObject, Action<T> action, T args)
        {
            ListenDestory(gameObject, () => { action(args); });
        }

        public static void Wait(float time, Action action)
        {
            IEnumerator Runner()
            {
                yield return new WaitForSeconds(time);
                action();
            }

            OnStartCoroutine(Runner());
        }

        public static void WaitFrameEnd(Action action)
        {
            IEnumerator Runner()
            {
                yield return new WaitForEndOfFrame();
                action();
            }

            OnStartCoroutine(Runner());
        }

        internal static ScheduleCancelToken Scheduled(float time, Action action)
        {
            ScheduleCancelToken scheduleCancelToken = new ScheduleCancelToken();
            instance.schedules.Add(new IntervalTimeSchedule(scheduleCancelToken, time, action));
            Client.Console.WriteLine("Add Schedule", instance.schedules.Count);
            return scheduleCancelToken;
        }


        internal static ScheduleCancelToken Countdown(int time, int interval, bool isadd, Action<int> callback)
        {
            ScheduleCancelToken scheduleCancelToken = new ScheduleCancelToken();
            CountDownSchedule countdown = new CountDownSchedule(time, interval, isadd, callback, scheduleCancelToken);
            instance.countdowns.Add(countdown);
            return scheduleCancelToken;
        }


        public static void RegisterConroutineStartu(Action<IEnumerator> startup, Action<Coroutine> stopup)
        {
            conoutineStartup = startup;
            conoutineStopup = stopup;
        }

        public static void UnregisterConroutineStartu()
        {
            conoutineStartup = null;
            conoutineStopup = null;
        }

        public static Coroutine OnStartCoroutine(IEnumerator enumerator)
        {
#if UNITY_EDITOR
            if (!Application.isPlaying)
            {
                conoutineStartup?.Invoke(enumerator);
                return default;
            }
#endif
            return instance.StartCoroutine(enumerator);
        }

        public static void OnStopCoroutine(Coroutine coroutine)
        {
            if (coroutine is null)
            {
                return;
            }
#if UNITY_EDITOR
            if (!Application.isPlaying)
            {
                conoutineStopup?.Invoke(coroutine);
                return;
            }
#endif
            instance.StopCoroutine(coroutine);
        }
    }
}